Repository Public
1import type { Metadata } from "next";2import Link from "next/link";3import { FilmGrid } from "@/components/FilmCard";4import { listFilms, listSeries, listTags } from "@/lib/films";5import { dict, href, type Locale } from "@/lib/i18n";67export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {8 const locale = (await params).locale as Locale;9 return { title: dict(locale).explore.title, description: dict(locale).explore.sub, alternates: { canonical: href(locale, "/explore"), languages: { en: "/explore", fr: "/fr/explore" } } };10}1112type SP = Record<string, string | undefined>;13const Chip = ({ on, to, children }: { on: boolean; to: string; children: React.ReactNode }) => <Link href={to} scroll={false} aria-current={on ? "true" : undefined} className="chip">{children}</Link>;14const Group = ({ label, children }: { label: string; children: React.ReactNode }) => (15 <div className="grid gap-2 sm:grid-cols-[110px_1fr] sm:items-start"><p className="label pt-2">{label}</p><div className="flex flex-wrap gap-2">{children}</div></div>16);17const KEYS = ["series", "tag", "lang", "kind", "max", "sort"] as const;1819export default async function Explore({ params, searchParams }: { params: Promise<{ locale: string }>; searchParams: Promise<SP> }) {20 const locale = (await params).locale as Locale;21 const sp = await searchParams;22 const t = dict(locale);23 const f = { series: sp.series, tag: sp.tag, lang: ["en", "fr", "bilingual"].includes(sp.lang ?? "") ? sp.lang : undefined, kind: ["fiction", "factual", "experimental"].includes(sp.kind ?? "") ? sp.kind : undefined, max: sp.max === "30" || sp.max === "45" ? sp.max : undefined, sort: sp.sort === "top" ? "top" : undefined };24 const [{ films }, series, tags] = await Promise.all([25 listFilms(locale, { series: f.series, tag: f.tag, language: f.lang, kind: f.kind, maxDuration: f.max ? Number(f.max) : undefined, sort: f.sort as "top" | undefined, limit: 48 }),26 listSeries(locale), listTags(locale),27 ]);28 const link = (patch: Partial<Record<(typeof KEYS)[number], string | undefined>>) => {29 const q = new URLSearchParams();30 for (const k of KEYS) { const v = k in patch ? patch[k] : f[k]; if (v) q.set(k, v); }31 const s = q.toString();32 return href(locale, `/explore${s ? `?${s}` : ""}`);33 };34 const active = KEYS.some((k) => f[k]);35 const kinds = [...new Set(tags.map((x) => x.kind))];3637 return (38 <div className="py-10">39 <h1 className="display text-5xl md:text-6xl">{t.explore.title}</h1>40 <p className="prose-film mt-3 text-lg">{t.explore.sub}</p>41 <div className="mt-8 space-y-4 border-y border-[var(--color-line-soft)] py-6">42 <Group label={t.nav.series}><Chip on={!f.series} to={link({ series: undefined })}>{t.explore.all}</Chip>{series.filter((s) => s.count > 0).map((s) => <Chip key={s.slug} on={f.series === s.slug} to={link({ series: f.series === s.slug ? undefined : s.slug })}>{s.title}</Chip>)}</Group>43 <Group label={t.explore.form}>{(["fiction", "factual", "experimental"] as const).map((k) => <Chip key={k} on={f.kind === k} to={link({ kind: f.kind === k ? undefined : k })}>{t.film[k]}</Chip>)}</Group>44 {kinds.map((kind) => (45 <Group key={kind} label={t.tagKinds[kind] ?? kind}>{tags.filter((x) => x.kind === kind).map((x) => <Chip key={x.slug} on={f.tag === x.slug} to={link({ tag: f.tag === x.slug ? undefined : x.slug })}>{x.label} <span className="ml-1 opacity-60">{x.count}</span></Chip>)}</Group>46 ))}47 <Group label={t.explore.language}>{(["en", "fr", "bilingual"] as const).map((k) => <Chip key={k} on={f.lang === k} to={link({ lang: f.lang === k ? undefined : k })}>{t.langs[k]}</Chip>)}</Group>48 <Group label={t.explore.length}><Chip on={f.max === "30"} to={link({ max: f.max === "30" ? undefined : "30" })}>{t.explore.under30}</Chip><Chip on={f.max === "45"} to={link({ max: f.max === "45" ? undefined : "45" })}>{t.explore.under45}</Chip></Group>49 <Group label={t.explore.sort}><Chip on={!f.sort} to={link({ sort: undefined })}>{t.explore.latest}</Chip><Chip on={f.sort === "top"} to={link({ sort: "top" })}>{t.explore.top}</Chip></Group>50 </div>51 <div className="mt-6 flex items-center justify-between text-sm text-[var(--color-faint)]">52 <p aria-live="polite">{films.length} {t.explore.results}</p>53 {active && <Link href={href(locale, "/explore")} className="underline hover:text-[var(--color-text)]">{t.explore.clear}</Link>}54 </div>55 <div className="mt-5">{films.length ? <FilmGrid films={films} locale={locale} /> : <p className="card p-8 text-[var(--color-muted)]">{t.explore.none}</p>}</div>56 </div>57 );58}59